package org.zjvis.datascience.common.graph.util;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.zjvis.datascience.common.graph.importer.ImporterCSV;
import org.zjvis.datascience.common.graph.importer.ImporterGML;
import org.zjvis.datascience.common.graph.model.GraphDataCheckException;
import org.zjvis.datascience.common.graph.model.GraphDataErrorCode;
import org.zjvis.datascience.common.graph.vo.AttrVO;
import org.zjvis.datascience.common.util.StringUtil;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class GraphFileUtil {

    public static boolean checkGraphFile(InputStream in, String fileName) throws IOException {
        JSONArray nodeArray = null;
        JSONArray linkArray = null;
        String fileType = "";
        if (fileName.endsWith(".json")) {

            String JSONString = StringUtil.readInputStreamToString(in, StandardCharsets.UTF_8);
            JSONObject obj = JSONArray.parseObject(JSONString);
            nodeArray = obj.getJSONArray("nodes");
            linkArray = obj.getJSONArray("links");
            fileType = "json";
        } else if (fileName.endsWith(".csv")) {
            JSONObject ret = ImporterCSV.parseCSV2JSON(in, null);
            JSONArray headArray = ret.getJSONArray("head");
            List<String> heads = new ArrayList<>();
            for (int i = 0; i < headArray.size() ; i++) {
                JSONObject head = headArray.getJSONObject(i);
                heads.add(head.getString("name"));
            }
            String loadType = checkHeads(heads);
            JSONArray data = ret.getJSONArray("data");
            if (loadType.equals("node")) {
                nodeArray = data;
            } else if (loadType.equals("link")) {
                linkArray = data;
            }
            fileType = "csv";
        } else if (fileName.endsWith(".gml")) {
            ImporterGML importerGML = new ImporterGML();
            importerGML.execute(in);
            nodeArray = importerGML.getNodes();
            linkArray = importerGML.getLinks();
            fileType = "gml";
        } else {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_FORMAT_ERROR, "");
        }

        Map<String, Map<String, Integer>> cIdAttrHelper = new HashMap<>();
        Map<String, Map<String, Integer>> eIdAttrHelper = new HashMap<>();
        Map<Integer, String> nId2Cid = new HashMap<>();
        if (nodeArray != null) {
            nodeArray.forEach( o -> checkNode(o, cIdAttrHelper, nId2Cid));
        }
        if (linkArray != null) {
            String finalFileType = fileType;
            linkArray.forEach(o -> checkLink(o, eIdAttrHelper, nId2Cid, finalFileType));
        }
        return true;
    }

    public static boolean checkNode(Object o, Map<String, Map<String, Integer>> cIdAttrHelper, Map<Integer, String> nId2Cid) throws GraphDataCheckException {
        JSONObject nodeObj = o instanceof JSONObject ? (JSONObject)o : (JSONObject)JSONObject.toJSON(o);
        Integer id = checkInteger(nodeObj, "id");
        String cid = nodeObj.getString("categoryId");
        if (id == null) {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_REQUIRED_KEY_MISS, "id");
        }
        if (cid == null) {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_REQUIRED_KEY_MISS, "categoryId");
        }
        List<AttrVO> attrs = new ArrayList<>();
        JSONObject attrsObj = checkJSONObject(nodeObj, "attributes");
        cIdAttrHelper.putIfAbsent(cid, new HashMap<>());
        if (attrsObj != null) {
            for (String key: attrsObj.keySet()) {
                Object val = attrsObj.get(key);
                if (val instanceof BigDecimal) {
                    val = ((BigDecimal) val).doubleValue();
                }
                int type = val instanceof String ? 12 : 2;
                attrs.add(
                        AttrVO.builder()
                                .key(key)
                                .value(val)
                                .type(type)
                                .build()
                );
                Map<String, Integer> cMap = cIdAttrHelper.get(cid);
                Object preType = cMap.putIfAbsent(key, type);
                if (preType != null && !preType.equals(type)) {
                    throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_ATTRS_TYPE_CONFLICT, key);
                }
            }
        }
        nId2Cid.put(id, cid);
        return true;
    }

    public static boolean checkLink(Object o, Map<String, Map<String, Integer>> eIdAttrHelper, Map<Integer, String> nId2Cid, String fileType) throws GraphDataCheckException {
        JSONObject linkObj = o instanceof JSONObject ? (JSONObject)o : (JSONObject)JSONObject.toJSON(o);
        Integer id = checkInteger(linkObj, "id");
        Integer source = checkInteger(linkObj, "source");
        Integer target = checkInteger(linkObj, "target");
        JSONObject attrsObj = checkJSONObject(linkObj, "attributes");
        if (id == null) {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_REQUIRED_KEY_MISS, "id");
        }
        if (source == null) {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_REQUIRED_KEY_MISS, "source");
        }
        if (target == null) {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_REQUIRED_KEY_MISS, "target");
        }

        if (!fileType.equals("csv")) {
            String srcCid = nId2Cid.get(source);
            String tarCid = nId2Cid.get(target);

            if (srcCid == null) {
                throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_NODE_NOT_FOUND, "source: " + source);
            }
            if (tarCid == null) {
                throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_NODE_NOT_FOUND, "target: " + target);
            }

            String eid = srcCid + "_" + tarCid;
            List<AttrVO> attrs = new ArrayList<>();
            eIdAttrHelper.putIfAbsent(eid, new HashMap<>());
            if (attrsObj != null) {
                for (String key: attrsObj.keySet()) {
                    Object val = attrsObj.get(key);
                    int type = val instanceof String ? 12 : 2;
                    attrs.add(
                            AttrVO.builder()
                                    .key(key)
                                    .value(val)
                                    .type(type)
                                    .build()
                    );
                    Map<String, Integer> eMap = eIdAttrHelper.get(eid);
                    Object preType = eMap.putIfAbsent(key, type);
                    if (preType != null && !preType.equals(type)) {
                        throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_ATTRS_TYPE_CONFLICT, "");
                    }
                }
            }
        }
        return true;
    }

    public static JSONObject checkJSONObject(JSONObject obj, String key) {
        try {
            JSONObject attrsObj = obj.getJSONObject(key);
            return attrsObj;
        } catch (Exception e1) {
            String str = obj.getString(key);
            if (str.startsWith("\"{") && str.endsWith("}\"")) {
                str = str.substring(1, str.length()-1);
                str = str.replaceAll("\"\"","\"");
                try {
                    JSONObject attrsObj = JSONObject.parseObject(str);
                    return attrsObj;
                } catch (Exception e2) {
                    throw e2;
                }
            }
        }
        return null;
    }

    public static String checkHeads(List<String> heads) {
        List<String> validNodeHead = Arrays.asList("id", "categoryId", "label", "attributes", "config");
        List<String> validLinkHead = Arrays.asList("id", "source", "target", "directed", "label", "weight", "attributes", "config");

        String loadType;
        if (!heads.contains("id")) {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_REQUIRED_KEY_MISS, "id");
        } else {
            if (heads.contains("categoryId")) {
                //check node file
                for (String head: heads) {
                    if (!validNodeHead.contains(head)) throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_CSV_HEAD_ERROR, head);
                }
                loadType = "node";
            }
            else if (heads.contains("source") && heads.contains("target")) {
                //check link file
                for (String head: heads) {
                    if (!validLinkHead.contains(head)) throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_CSV_HEAD_ERROR, head);
                }
                loadType = "link";
            }
            else throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_CSV_HEAD_ERROR, "缺少必填字段");
        }
        return loadType;
    }

    public static Integer checkInteger(JSONObject obj, String key) {
        Integer id;
        try {
            id = obj.getInteger(key);
        } catch (NumberFormatException e) {
            throw new GraphDataCheckException(GraphDataErrorCode.GRAPH_LOAD_REQUIRED_INTEGER_ID, key + ": " + obj.get(key));
        }
        return id;
    }

}
